/*
 * Copyright 2008 Jeff Dwyer
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.apress.progwt.client.college.gui;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.allen_sauer.gwt.log.client.Log;
import com.apress.progwt.client.college.gui.ext.ClientMouseImpl;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.ChangeListenerCollection;
import com.google.gwt.user.client.ui.MouseListener;
import com.google.gwt.user.client.ui.MouseWheelListener;
import com.google.gwt.user.client.ui.MouseWheelVelocity;
import com.google.gwt.user.client.ui.SourcesChangeEvents;
import com.google.gwt.user.client.ui.SourcesMouseEvents;
import com.google.gwt.user.client.ui.SourcesMouseWheelEvents;
import com.google.gwt.user.client.ui.Widget;

public abstract class ViewPanel extends AbsolutePanel implements
        MouseListener, SourcesChangeEvents, MouseWheelListener {

    private class RedrawParams {
        public int centerX;
        public int centerY;
        public int halfHeight;
        public int halfWidth;
        public double yScale;
    }

    protected int backX = 0;
    protected int backY = 0;
    private ChangeListenerCollection changeCollection = new ChangeListenerCollection();

    private int curbackX = 0;

    private int curbackY = 0;

    protected double currentScale = 1;

    private boolean doYTranslate = true;

    private boolean doZoom;

    protected boolean dragEnabled = true;

    private boolean dragging;

    private int dragStartX;

    private int dragStartY;
    private EventBackdrop focusBackdrop;

    protected List objects = new ArrayList();

    private ClientMouseImpl clientMouseImpl;

    public ViewPanel() {
        super();

        clientMouseImpl = (ClientMouseImpl) GWT
                .create(ClientMouseImpl.class);

        focusBackdrop = new EventBackdrop();
        makeThisADragHandle(focusBackdrop);
        add(focusBackdrop, 0, 0);

    }

    public void addChangeListener(ChangeListener listener) {
        changeCollection.add(listener);
    }

    public void addObject(RemembersPosition rp) {
        add(rp.getWidget(), (int) rp.getLeft(), rp.getTop());
        objects.add(rp);
    }

    protected void centerOn(int x, int y) {

        // Log.debug("centering on "+x+" "+ y);

        int width = getWidth();
        int height = getHeight();

        int halfWidth = width / 2;
        int halfHeight = height / 2;

        int left = (int) (x * currentScale * getXSpread()) - halfWidth;
        int top = (int) (y * currentScale) - halfHeight;

        // Log.debug("P.X "+P.X+" HW "+HALFWIDTH+" "+LEFT);
        // Log.debug("left "+left+" top "+top);

        // intuitively this is (left - curbackX) but things are reversed
        // int dx = left + curbackX;
        // int dy = top + curbackY;

        int dx = left + curbackX;
        int dy = top + curbackY;
        moveBy(dx, dy);

    }

    /**
     * 
     */
    protected void centerOnMouse() {

        int halfWidth = getWidth() / 2;
        int halfHeight = getHeight() / 2;

        // lastx, lasty are just a backup in case this doesn't work.
        // They're not a great backup,
        // because if child objects have obscured us we won't be getting
        // mouseMove events and we'll
        // be weird relative x & y's from senders anyway.
        // BUT it turns out that FF clientX & Y are rogered for
        // ScrollWheel events, -> replace-with
        // MozillaImpl
        Event curEvent = DOM.eventGetCurrentEvent();

        int lastx = clientMouseImpl.getClientX(curEvent);
        int lasty = clientMouseImpl.getClientY(curEvent);

        int dx = lastx - getAbsoluteLeft() - halfWidth;
        int dy = lasty - getAbsoluteTop() - halfHeight;

        Log.debug("ViewPanel.centerOnMouse last x " + lastx + " absLeft "
                + getAbsoluteLeft() + " curbackx " + curbackX + " dx "
                + dx);
        Log.debug("ViewPanel.centerOnMouse last y " + lasty + " absTop "
                + getAbsoluteTop() + " curbacky " + curbackY + " dy "
                + dy);

        moveBy(dx, dy);

    }

    /**
     * don't let a regular clear() happen or you'll lose the focusBackdrop
     */
    public void clear() {
        // Log.debug("calling our clear()");
        for (Iterator iter = objects.iterator(); iter.hasNext();) {
            Widget w = (Widget) iter.next();
            remove(w);
            iter.remove();
        }
    }

    private void endDrag() {
        if (dragging) {
            // Log.debug("(old)back x "+backX+" cur(new)
            // "+curbackX);
            // Log.debug("(old)back y "+backY+" cur(new)
            // "+curbackY);
            backX = curbackX;
            backY = curbackY;
        }
        dragging = false;
    }

    /**
     * Make sure that we're zoomed to 'scale' or higher
     * 
     * return the value that we settle on
     */
    public double ensureZoomOfAtLeast(double scale) {
        if (scale > currentScale) {
            zoomTo(scale);
        }
        return currentScale;
    }

    protected void finishZoom(double oldScale) {

        setBackground(currentScale);

        int width = getWidth();
        int height = getHeight();

        int centerX = getCenterX(oldScale, width);
        int centerY = getCenterY(oldScale, height);

        // moveTo(centerX, centerY);

        int halfWidth = width / 2;
        int halfHeight = height / 2;
        reCenter(centerX, centerY, currentScale, halfWidth, halfHeight);

        redraw();

        postZoomCallback(currentScale);

        redraw();

    }

    public int getBackX() {
        return backX;
    }

    public int getBackY() {
        return backY;
    }

    protected int getCenterX() {
        return getCenterX(currentScale, getWidth());
    }

    protected int getCenterX(double scaleToUse, int width) {
        int halfWidth = width / 2;
        int centerX = (int) ((-curbackX + halfWidth) / (scaleToUse * getXSpread()));

        // Log.debug("get Center X "+scaleToUse+" "+(-curbackX +
        // halfWidth)+" "+centerX);
        return centerX;
    }

    protected int getCenterY() {
        return getCenterY(currentScale, getHeight());
    }

    protected int getCenterY(double scaleToUse, int height) {
        int halfHeight = height / 2;
        int centerY = (int) ((-curbackY + halfHeight) / scaleToUse);
        return centerY;
    }

    public int getCurbackX() {
        return curbackX;
    }

    public int getCurbackY() {
        return curbackY;
    }

    public EventBackdrop getFocusBackdrop() {
        return focusBackdrop;
    }

    protected abstract int getHeight();

    public int[] getLongLatForXY(int absLeft, int absTop) {

        int oceanLeft = getBackX();
        int oceanTop = getBackY();

        int newLeft = absLeft - oceanLeft;
        int newTop = absTop - oceanTop;

        int lng = (int) (newLeft / currentScale);
        int lat = (int) (newTop / currentScale);

        return new int[] { lng, lat };

    }

    private RedrawParams getParams(int dy) {
        RedrawParams rd = new RedrawParams();
        rd.yScale = 1;
        if (isDoYTranslate()) {
            curbackY = -dy + backY;
            rd.yScale = currentScale;
        }

        DOM.setStyleAttribute(getElement(), "backgroundPosition",
                curbackX + "px " + curbackY + "px");

        int width = getWidth();
        int height = getHeight();

        rd.halfWidth = width / 2;
        rd.halfHeight = height / 2;

        rd.centerX = getCenterX(currentScale, width);
        rd.centerY = getCenterY(rd.yScale, height);
        return rd;
    }

    public int getPositionX(int left) {

        // Log.debug("getPositionX " + left + " " + currentScale
        // + " " + getXSpread() + " "
        // + curbackX);
        return (int) (left * currentScale * getXSpread()) + curbackX;
    }

    public int getPositionXFromGUIX(int guix) {

        Log.debug("getPositionXFromGuiX " + guix + " " + currentScale
                + " " + getXSpread() + " " + curbackX);
        return (int) ((guix - curbackX) / currentScale / (double) getXSpread());
    }

    protected abstract int getWidth();

    /**
     * Basically an extra zoom factor. Spread to the size of the
     * background. Overridden to 600 for ZoomableTimeline
     * 
     * @return
     */
    protected int getXSpread() {
        return 1;
    }

    public boolean isDoYTranslate() {
        return doYTranslate;
    }

    public boolean isDoZoom() {
        return doZoom;
    }

    protected void makeThisADragHandle(Widget widget) {

        if (doZoom) {
            if (widget instanceof SourcesMouseWheelEvents) {
                SourcesMouseWheelEvents wheeler = (SourcesMouseWheelEvents) widget;
                wheeler.addMouseWheelListener(this);
            }
        }
        if (widget instanceof SourcesMouseEvents) {
            SourcesMouseEvents mouser = (SourcesMouseEvents) widget;
            mouser.addMouseListener(this);
        }
    }

    /**
     * This moves the background and then sets the back position. Call
     * this when you want a move.
     * 
     * @param dx
     * @param dy
     */
    public void moveBy(int dx, int dy) {
        moveByDelta(dx, dy);

        // this was normally set in finishDrag()
        backX = curbackX;
        backY = curbackY;
    }

    /**
     * Internal move method. Doesn't actually 'finish' the move. This
     * helps us make dragging smoother.
     * 
     * Use moveBy() unless you'll finish yourself.
     * 
     * Takes dx, dy as SouthEast (+,+) NW (-,-)
     * 
     * @param dx
     * @param dy
     */
    protected void moveByDelta(int dx, int dy) {
        curbackX = -dx + backX;

        RedrawParams rd = getParams(dy);

        // Log.debug("ViewPanel.moveByDelta dx " + dx + " dy " +
        // dy);

        for (Iterator iter = objects.iterator(); iter.hasNext();) {
            Object o = iter.next();
            RemembersPosition rp = (RemembersPosition) o;

            redrawObj(rp, rd);
        }

        moveOccurredCallback();
        // Log.debug("moved "+curbackX+" "+curbackY);

    }

    protected void moveOccurredCallback() {
    }

    /**
     * Do an absolute move
     * 
     * @param x
     * @param y
     */
    public void moveTo(int x, int y) {
        int dx = backX - x;
        int dy = backY - y;

        moveByDelta(dx, dy);

        // this was normally set in finishDrag()
        backX = curbackX;
        backY = curbackY;
    }

    /**
     * Override this if you want object move event processing
     * 
     * @param o
     * @param halfWidth
     * @param halfHeight
     * @param centerX
     * @param centerY
     */
    protected void objectHasMoved(RemembersPosition o, int halfWidth,
            int halfHeight, int centerX, int centerY) {
    }

    public void onMouseDown(Widget sender, int x, int y) {
        // Log.debug("down "+(sender instanceof Island) +" x
        // "+x+" y "+y +" "+dragging);

        // Log.debug("mouse downd " + GWT.getTypeName(sender) + "
        // x " + x + " y " + y + " "
        // + sender.getAbsoluteLeft() + " " + sender.getAbsoluteTop());

        // if (!dragEnabled || sender instanceof FocusPanel) {
        dragging = true;
        // }

        dragStartX = x + sender.getAbsoluteLeft();
        dragStartY = y + sender.getAbsoluteTop();

        // Log.debug("down " + GWT.getTypeName(sender) + "dsx " +
        // dragStartX + " dsy "
        // + dragStartY + " x " + x + " y " + y + " " +
        // sender.getAbsoluteLeft() + " "
        // + sender.getAbsoluteTop());

        unselect();
    }

    public void onMouseEnter(Widget sender) {
    }

    public void onMouseLeave(Widget sender) {
        endDrag();
    }

    public void onMouseMove(Widget sender, int x, int y) {
        int lastx = x + sender.getAbsoluteLeft();
        int lasty = y + sender.getAbsoluteTop();
        clientMouseImpl.setLastXY(lastx, lasty);

        int dx = dragStartX - x - sender.getAbsoluteLeft();
        int dy = dragStartY - y - sender.getAbsoluteTop();

        // Log.debug("move "+(sender instanceof Island) +" x
        // "+x+" y "+y +" "+dragging);
        if (dragging) {
            moveByDelta(dx, dy);
            changeCollection.fireChange(this);
        }
    }

    public void onMouseUp(Widget sender, int x, int y) {
        // Log.debug("up " + GWT.getTypeName(sender) + "dsx " +
        // dragStartX + " dsy "
        // + dragStartY + " x " + x + " y " + y + " " +
        // sender.getAbsoluteLeft() + " "
        // + sender.getAbsoluteTop());

        // Log.debug("up "+(sender instanceof Island) +" x "+x+"
        // y "+y +" "+dragging);
        endDrag();
    }

    public void onMouseWheel(Widget sender, MouseWheelVelocity velocity) {
        if (velocity.isSouth()) {
            zoomOut();
        } else {
            centerOnMouse();
            zoomIn();
        }
    }

    protected void postZoomCallback(double currentScale) {
    }

    private void reCenter(int centerX, int centerY, double scale,
            int halfWidth, int halfHeight) {

        // Log.debug("recenter\nback X "+backX+" backy "+backY);
        // Log.debug("center X "+centerX+" cy "+centerY+" scale
        // "+scale);

        // Log.debug("hw "+halfWidth+" hh "+halfHeight);
        // backX = halfWidth - halfWidth/currentScale;

        int newCenterX = (int) (centerX * scale * getXSpread());
        int newCenterY = (int) (centerY * scale);

        // Log.debug("new center X "+newCenterX+" "+newCenterY);

        backX = -(newCenterX - halfWidth);
        backY = -(newCenterY - halfHeight);

        // Log.debug("Newback X "+backX+" NEWbacky "+backY);

    }

    /**
     * redraw all
     */
    public void redraw() {
        // Log.debug("ViewPanel.redraw()");
        moveBy(0, 0);
    }

    /**
     * redraw a single object
     * 
     * @param rp
     */
    public void redraw(RemembersPosition rp) {
        Log.debug("ViewPanel.redraw(RP) ");
        RedrawParams rd = getParams(0);
        redrawObj(rp, rd);
        moveOccurredCallback();
    }

    private void redrawObj(RemembersPosition rp, RedrawParams params) {
        // Log.debug("found: "+GWT.getTypeName(rp));

        // Log.debug("Left "+isle.getLeft()+" Top
        // "+isle.getTop());
        // Log.debug("cur "+curbackX+" cury "+curbackY);

        // setWidgetPosition(rp.getWidget(),(int)((rp.getLeft()+curbackX)*currentScale),
        // (int)((rp.getTop()+curbackY)*currentScale));

        // Log.debug("move "+rp.getLeft()+"
        // "+(int)((rp.getLeft())*currentScale)+"
        // "+(int)((rp.getLeft())*currentScale*getXSpread())+" cs
        // "+currentScale);
        try {

            setWidgetPosition(rp.getWidget(), getPositionX(rp.getLeft()),
                    (int) ((rp.getTop()) * params.yScale) + curbackY);

        } catch (RuntimeException e) {

            Log.error("ERROR: ViewPanel. couldn't move: "
                    + rp.getWidget());

            throw e;
        }
        objectHasMoved(rp, params.halfWidth, params.halfHeight,
                params.centerX, params.centerY);
    }

    public void removeChangeListener(ChangeListener listener) {
        changeCollection.remove(listener);
    }

    public boolean removeObj(Widget w) {
        Log.debug("ViewPanel.remove " + GWT.getTypeName(w));
        super.remove(w);
        return objects.remove(w);
    }

    protected abstract void setBackground(double scale);

    public void setDoYTranslate(boolean doYTranslate) {
        this.doYTranslate = doYTranslate;
    }

    public void setDoZoom(boolean doZoom) {
        this.doZoom = doZoom;
        makeThisADragHandle(focusBackdrop);
    }

    protected void unselect() {

    }

    public void zoomIn() {
        double oldScale = currentScale;

        currentScale *= 2;

        finishZoom(oldScale);
    }

    public void zoomOut() {
        double oldScale = currentScale;

        currentScale /= 2;

        finishZoom(oldScale);
    }

    public void zoomTo(double scale) {
        if (scale == currentScale) {
            return;
        }
        double oldScale = currentScale;

        currentScale = scale;

        finishZoom(oldScale);

    }
}
